//@author A0097758E
/**
 * This class acts as a custom Data Structure to house all Task objects.
 * Assisted by the following helper classes:
 * 1) ActionHistory
 * 2) TaskHistory
 * 3) Task
 * 4) FileHandler
 */

package storage;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;

import sharedUtils.Constants;
import sharedUtils.Logger;

public class TaskList {
	private static final int DEFAULT_GET_DAYWEEKMONTH_CRITERIA = 2;
	private static final int GET_DAY = 1;
	private static final int GET_WEEK = 2;
	private static final int GET_MONTH = 3;
	
	private int nextTaskId;
	private boolean isSavingEnabled;
	
	private FileHandler fh;
	private ArrayList<TaskHistory> list;
	private ActionHistory pastActions;
	
	/**
	 * Default constructor. Creates an empty TaskList. 
	 * No saving to persistent storage is enabled.
	 */
	public TaskList() {
		Logger.log("Init TaskList without args", 
				TaskList.class.getSimpleName(), "TaskList()");
		
		fh = null;
		isSavingEnabled = false;
		
		this.list = new ArrayList<TaskHistory>();
		this.pastActions = new ActionHistory();
		initNextTaskId();
	}
	
	/**
	 * Retrieves contents of given file and creates a TaskList 
	 * populated by the saved tasks. Avoid using absolute filepaths as 
	 * it may not work well on *nix platforms 
	 * (if coded/tested on Windows). Saving is enabled.
	 * 
	 * @param fileLoc The relative filepath and filename of the save file.
	 */
	public TaskList(String fileLoc) {
		Logger.log("Init TaskList with fileName: " + fileLoc, 
				TaskList.class.getSimpleName(), 
				"TaskList(String fileLoc)");
		
		isSavingEnabled = true;
		
		if(fileLoc != null) {
			fh = new FileHandler(fileLoc);
		} else {
			fh = new FileHandler(Constants.DEFAULT_FILE_LOC);
		}
		
		/**
		 * I can't call the other constructors unless it's the first statement =<
		 */
		if(!fh.isInit()) {
			fh = null;
			list = new ArrayList<TaskHistory>();
		} else {
			this.list = createHistoryArrayList(fh.read());
			
			if(list == null) {
				list = new ArrayList<TaskHistory>();
			}
		}
		
		this.pastActions = new ActionHistory();
		initNextTaskId();
	}
	
	/**
	 * Creates a TaskList from an ArrayList of Task objects. 
	 * No saving allowed.
	 * 
	 * @param noHistoryList An ArrayList of Task objects 
	 */
	public TaskList(ArrayList<Task> noHistoryList) {
		Logger.log("Init TaskList with ArrayList<Task>", 
				TaskList.class.getSimpleName(), 
				"TaskList(ArrayList<Task> noHistoryList)");
		
		isSavingEnabled = false;
		fh = null;
		
		this.list = createHistoryArrayList(noHistoryList);
		
		if(list == null) {
			list = new ArrayList<TaskHistory>();
		}
		
		this.pastActions = new ActionHistory();
		initNextTaskId();
	}
	
	/**
	 * Sets nextTaskId based on the last taskId of the last task
	 * in the TaskList. If TaskList is empty, nextTaskId is set to 1. 
	 * This method should only be invoked by the constructors.
	 */
	private void initNextTaskId() {
		Logger.log("Init new task Id", TaskList.class.getSimpleName(), 
				"initNextTaskId()");
		
		if(list != null && list.size() > 0) {
			nextTaskId = list.get(list.size() - 1).getTask().getId() + 1;
		} else {
			nextTaskId = 1;
		}
	}
	
	/**
	 * Helper method to convert an ArrayList of Task objects into 
	 * an ArrayList of TaskHistory.
	 * 
	 * @param noHistoryList The ArrayList of Task objects to be converted.
	 * @return An ArrayList of TaskHistory objects converted from the given 
	 * ArrayList of Task objects. Returns null on error.
	 */
	public static ArrayList<TaskHistory> createHistoryArrayList(ArrayList<Task> noHistoryList) {
		/**
		 * Check arguments
		 */
		if(noHistoryList == null) {
			return null;
		}
		
		/**
		 * Logic
		 */
		Logger.log("Changing contents of ArrayList<Task> -> ArrayList<TaskHistory>", 
				TaskList.class.getSimpleName(), "createHistoryArrayList()");
		
		ArrayList<TaskHistory> tempList = new ArrayList<TaskHistory>();
		
		for(int i=0; i<noHistoryList.size(); i++) {
			tempList.add(new TaskHistory(noHistoryList.get(i)));
		}
		
		return tempList;
	}
	
	/**
	 * Saves given ArrayList of Task objects to persistent storage. 
	 * If Constants.IOEXCEPTION_TRIGGERED was returned, it means the 
	 * file was not saved properly and should not be referenced until corrected.
	 * 
	 * @param al The ArrayList of Task objects to be saved to persistent storage.
	 * @return Returns one of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE, Constants.UNABLE_TO_SAVE, 
	 * Constants.FILEHANDLER_FAILED_WRITE_FILE_DOES_NOT_EXIST, 
	 * Constants.INVALID_NULL_ARG, Constants.IOEXCEPTION_TRIGGERED
	 */
	private int saveToFile(ArrayList<Task> al) {
		/**
		 * Check arguments
		 */
		if(checkIfNull(al) != 0) {
			return Constants.INVALID_NULL_ARG;
		}
		
		/**
		 * Logic
		 */
		Logger.log("Saving changes to persistent storage.", 
				TaskList.class.getSimpleName(), "saveToFile()");
		
		if(!isSavingEnabled) {
			return Constants.SUCCESS_ERROR_CODE;
		} if(fh == null || !fh.isInit()) {
			return Constants.UNABLE_TO_SAVE;
		}
		
		return fh.write(al);
	}
	
	/**
	 * Returns the next Task Id that will be associated to the next 
	 * incoming Task object.
	 * 
	 * @return TaskId that will be paired to the next Task.
	 */
	public int getNextTaskId() {
		Logger.log("Returning next task Id", TaskList.class.getSimpleName(),
				"getNextTaskId()");
		
		return nextTaskId;
	}
	
	/**
	 * Returns all valid Tasks in this TaskList.
	 * 
	 * @return An ArrayList of Task objects containing all valid Tasks. 
	 * Returns null on error.
	 */
	public ArrayList<Task> getAll() {
		Logger.log("Retrieving all valid entries", 
				TaskList.class.getSimpleName(), "getAll()");
		
		ArrayList<Task> output = new ArrayList<Task>(list.size());
		
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && list.get(i).getTask() != null && 
					list.get(i).getTask().getIsValid() &&
					!output.add(list.get(i).getTask())) {
				return null;
			}
		}
		
		return output;
	}
	
	/**
	 * Returns all valid Tasks that is due today (on runtime).
	 * 
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due today. Returns null on error. 
	 */
	public ArrayList<Task> getAllToday() {
		Logger.log("Retrieving all entries due today",
				TaskList.class.getSimpleName(), "getAllToday()");
		
		return getAllByDay(1);
	}
		
	/**
	 * Returns all valid Tasks that is due within the next 2 days, 
	 * inclusive of today (on runtime).
	 * 
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next 2 days. Returns null on error. 
	 */
	public ArrayList<Task> getAllByDay() {
		Logger.log("Retrieving all entries due next 2 days", 
				TaskList.class.getSimpleName(), "getAllByDay()");
		
		return getAllByDay(DEFAULT_GET_DAYWEEKMONTH_CRITERIA);
	}
	
	/**
	 * Returns all valid Tasks that is due within the next 2 weeks
	 * (i.e. 14 days), inclusive of today (on runtime).
	 * 
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next 2 weeks. Returns null on error. 
	 */
	public ArrayList<Task> getAllByWeek() {
		Logger.log("Retrieving all entries due next 2 weeks", 
				TaskList.class.getSimpleName(), "getAllByWeek()");
		
		return getAllByWeek(DEFAULT_GET_DAYWEEKMONTH_CRITERIA);
	}
	
	/**
	 * Returns all valid Tasks that is due within the next 2 months, 
	 * inclusive of today (on runtime).
	 * 
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next 2 months. Returns null on error. 
	 */
	public ArrayList<Task> getAllByMonth() {
		Logger.log("Retrieving all entries due next 2 months", 
				TaskList.class.getSimpleName(), "getAllByMonth()");
		
		return getAllByMonth(DEFAULT_GET_DAYWEEKMONTH_CRITERIA);
	}
	
	/**
	 * Returns all valid Tasks that is due within the next X days, 
	 * inclusive of today (on runtime).
	 * 
	 * @param howManyDays X. The number of days to search for.
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next X days. Returns null on error. 
	 */
	public ArrayList<Task> getAllByDay(int howManyDays) {
		Logger.log("Retrieving all entries due next " + howManyDays + " days", 
				TaskList.class.getSimpleName(), "getAllByDay(int howManyDays)");
		
		return getAllByTime(GET_DAY, howManyDays);
	}
	
	/**
	 * Returns all valid Tasks that is due within the next X weeks 
	 * (i.e. 7X days), inclusive of today (on runtime).
	 * 
	 * @param howManyWeeks X. The number of weeks to search for.
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next X weeks. Returns null on error. 
	 */
	public ArrayList<Task> getAllByWeek(int howManyWeeks) {
		Logger.log("Retrieving all entries due next " + howManyWeeks + " weeks", 
				TaskList.class.getSimpleName(), "getAllByWeek(int howManyWeeks)");
		
		return getAllByTime(GET_DAY, howManyWeeks*7);
	}
	
	/**
	 * Returns all valid Tasks that is due within the next X months, 
	 * inclusive of today (on runtime).
	 * 
	 * @param howManyMonths X. The number of months to search for.
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next X months. Returns null on error. 
	 */
	public ArrayList<Task> getAllByMonth(int howManyMonths) {
		Logger.log("Retrieving all entries due next " + howManyMonths + " months", 
				TaskList.class.getSimpleName(), "getAllByMonth(int howManyMonths)");
		
		return getAllByTime(GET_MONTH, howManyMonths);
	}
		
	/**
	 * Returns all valid Tasks that is due within the next X days/weeks/months, 
	 * depending on the mode given, inclusive of today (on runtime).
	 * 
	 * @param MODE_CONSTANT The metric to search for (days/weeks/months). 
	 * Valid values are TaskList.GET_DAY, TaskList.GET_WEEK, TaskList.GET_MONTH.
	 * @param howMany X. The number of days/weeks/months to search for.
	 * @return An ArrayList of Task objects containing all valid Tasks 
	 * due within the next X days/weeks/months, depending on the mode given. 
	 * Returns null on error.
	 */
	private ArrayList<Task> getAllByTime(int MODE_CONSTANT, int howMany) {
		/**
		 * Checking Arguments
		 */
		if(howMany <= 0 || MODE_CONSTANT < 0) {
			return new ArrayList<Task>();
		}
		
		/**
		 * Initialization
		 */
		Logger.log("Retrieving all entries due next " + howMany + " METRIC: " + MODE_CONSTANT,
				TaskList.class.getSimpleName(), "getAllByTime()");
		
		ArrayList<Task> output = new ArrayList<Task>(list.size());
		
		GregorianCalendar taskDate;
		GregorianCalendar today = new GregorianCalendar();
		GregorianCalendar referenceDate = new GregorianCalendar();
		
		/**
		 * Mode selection
		 */
		switch(MODE_CONSTANT) {
			case GET_DAY: {
				referenceDate.add(Calendar.DAY_OF_YEAR, howMany);
				break;
			}
			
			case GET_WEEK: {
				referenceDate.add(Calendar.WEEK_OF_YEAR, howMany);
				break;
			}
			
			case GET_MONTH:
			default: {
				referenceDate.add(Calendar.MONTH, howMany);
			}
		}		
		
		/**
		 * Logic
		 */
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && list.get(i).getTask() != null && 
					list.get(i).getTask().getIsValid() && 
					list.get(i).getTask().isEndDateSet() &&
					list.get(i).getTask().getEndDate() != null) {				
				taskDate = list.get(i).getTask().getEndDate();
				
				if(today.getTimeInMillis() <= taskDate.getTimeInMillis() &&
						referenceDate.getTimeInMillis() >= taskDate.getTimeInMillis() && 
						!output.add(list.get(i).getTask())) {
					return null;
				}
			}
		}
		
		return output;
	}
	
	/**
	 * Helper method. Checks if given integer falls within the range 
	 * of TaskList's indexes.
	 * 
	 * @param i Given index to test.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE or 
	 * Constants.INVALID_INDEX_ARG.
	 */
	private int checkIfWithinSizeOfTaskList(int i) {
		if(i < 0 || i > list.size() - 1) {
			return Constants.INVALID_INDEX_ARG;
		}
		
		return Constants.SUCCESS_ERROR_CODE;
	}
	
	/**
	 * Helper method. Checks if given object is null.
	 * Really silly to put this as a method, huh?
	 * 
	 * @param o Given object to test.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE or 
	 * Constants.INVALID_NULL_ARG.
	 */
	private int checkIfNull(Object o) {
		if(o == null) {
			return Constants.INVALID_NULL_ARG;
		}
		
		return Constants.SUCCESS_ERROR_CODE;
	}
	
	/**
	 * Helper method. Checks if given task has a non-null startDate but a 
	 * null endDate. This implies a deadline task, but the fields were 
	 * incorrectly placed. This method will help to swap them in such cases.
	 * 
	 * @param t Given Task object to check
	 */
	private void checkSEDateSwappable(Task t) {
		if(t != null && t.getStartDate() != null && t.getEndDate() == null) {
			Logger.log("Checking if Task sDate and eDate are swappable", 
					TaskList.class.getSimpleName(), "checkSEDateSwappable()");
			
			/**
			 * Only set start date and no end date, meaning that
			 * the task is a deadline-bound task. Need to swap them.
			 * 
			 * c = a;
			 * a = b;
			 * b = c;
			 */
			
			GregorianCalendar tempStartCal = t.getStartDate();
			boolean tempIsStartDateValid = t.isStartDateSet();
			boolean tempIsStartTimeValid = t.isStartTimeSet();
			
			t.setStartDate(t.getEndDate());
			t.setStartDateSet(t.isEndDateSet());
			t.setStartTimeSet(t.isEndTimeSet());
			
			t.setEndDate(tempStartCal);
			t.setEndDateSet(tempIsStartDateValid);
			t.setEndTimeSet(tempIsStartTimeValid);
		}
	}
	
	/**
	 * Adds the given Task to the TaskList, creating a TaskHistory 
	 * object to house the Task in the process.
	 * 
	 * @param t The given Task object to be added to this TaskList.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.INVALID_INDEX_ARG, 
	 * Constants.INVALID_NULL_ARG, 
	 * Constants.TASKLIST_FAILED_ADD_TO_AL, 
	 * Constants.TASKLIST_FAILED_ADD_TO_AH, 
	 * Constants.TASKLIST_INVALID_TASK_NO_INT_TITLE, 
	 * Constants.TASKLIST_FAILED_ADD_DUPLICATE, 
	 * Constants.UNABLE_TO_SAVE, 
	 * Constants.FILEHANDLER_FAILED_WRITE_FILE_DOES_NOT_EXIST, 
	 * Constants.INVALID_NULL_ARG or
	 * Constants.IOEXCEPTION_TRIGGERED.
	 */
	public int add(Task t) {
		/**
		 * Check arguments
		 */
		int inputValidResult;
		
		inputValidResult = checkIfNull(t);
		if(inputValidResult != 0) {
			return inputValidResult;
		}
		
		inputValidResult = checkIfValidTask(t);
		if(inputValidResult != 0) {
			return inputValidResult;
		}
		
		inputValidResult = checkIfExists(t);
		if(inputValidResult != 0) {
			return inputValidResult;
		}
		
		
		/**
		 * Logic
		 */
		Logger.log("Adding new Task: " + t.getTitle() + " to TaskList", 
				TaskList.class.getSimpleName(), "add()");
		
		checkSEDateSwappable(t);
		t.setId(nextTaskId);
		
		if(!list.add(new TaskHistory(t))) {
			return Constants.TASKLIST_FAILED_ADD_TO_AL;
		} else {
			nextTaskId++;
		}
		
		if(pastActions.add(list.size()-1) != 0) {
			return Constants.TASKLIST_FAILED_ADD_TO_AH;
		}
		
		return saveToFile(getAll());
	}

	/**
	 * Checks if the given Task object has a title consisting of all integers. 
	 * Since titles would be considered as invalid as they will interfere with 
	 * logic that uses TaskId as an argument.
	 * 
	 * @param t The given Task object to be checked.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.INVALID_NULL_ARG or  
	 * Constants.TASKLIST_INVALID_TASK_NO_INT_TITLE
	 */
	private int checkIfValidTask(Task t) {
		if(checkIfNull(t) != 0 || checkIfNull(t.getTitle()) != 0) {
			return Constants.INVALID_NULL_ARG;
		} else if(t.getTitle().matches(Constants.POSITIVE_INTEGER_REGEX)) {
			return Constants.TASKLIST_INVALID_TASK_NO_INT_TITLE;
		}		
		
		return Constants.SUCCESS_ERROR_CODE;
	}

	/**
	 * Check if there's a Task in the TaskList with the same title 
	 * as the given Task. TaskList will not allow Task entries 
	 * with duplicate titles.
	 * 
	 * @param t The given Task object to be checked.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.INVALID_NULL_ARG or  
	 * Constants.TASKLIST_FAILED_ADD_DUPLICATE
	 */
	private int checkIfExists(Task t) {
		/**
		 * Check arguments
		 */
		if(checkIfNull(t) != 0) {
			return Constants.INVALID_NULL_ARG;
		}
		
		Logger.log("Checking if Task: " + t.getTitle() + " already exists in TaskList", 
				TaskList.class.getSimpleName(), "checkIfExists()");
		
		/**
		 * Logic
		 */
		ArrayList<Task> tempTaskAl = searchForTitle(t.getTitle());
		
		if(tempTaskAl == null || tempTaskAl.size() == 0) {
			return Constants.SUCCESS_ERROR_CODE;
		} else if(tempTaskAl.get(0).getTitle().equalsIgnoreCase(t.getTitle())) {
			return Constants.TASKLIST_FAILED_ADD_DUPLICATE;
		} else {
			return Constants.SUCCESS_ERROR_CODE;
		}
	}
	
	/**
	 * Searches TaskList for the Task associated with the given 
	 * taskId and updates the state of said Task to the given Task.
	 * 
	 * @param taskId TaskId of Task to be updated.
	 * @param t New state of Task to overwrite to.
	 * @return One of the following error codes: 
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.INVALID_NULL_ARG, 
	 * Constants.TASKLIST_FAILED_ADD_DUPLICATE, 
	 * Constants.TASKLIST_INVALID_TASK_NO_INT_TITLE, 
	 * Constants.TASKLIST_FAILED_UPDATE_TO_AL, 
	 * Constants.TASKLIST_FAILED_UPDATE_TO_AH, 
	 * Constants.UNABLE_TO_SAVE, 
	 * Constants.FILEHANDLER_FAILED_WRITE_FILE_DOES_NOT_EXIST, 
	 * Constants.INVALID_NULL_ARG or
	 * Constants.IOEXCEPTION_TRIGGERED
	 */
	public int update(int taskId, Task t) {
		/**
		 * Check argument
		 */
		int inputValidResult, index;
		
		inputValidResult = checkIfNull(t);
		if(inputValidResult != 0) {
			return inputValidResult;
		}
		
		inputValidResult = getIndexOf(taskId);
		if(inputValidResult < 0) {
			return Constants.TASKLIST_FAILED_UPDATE_INVALID_TASK_ID;
		} else {
			index = inputValidResult;
		}
		
		inputValidResult = checkIfValidTask(t);
		if(inputValidResult != 0) {
			return inputValidResult;
		}
		
		// checkSEDateSwappable(t);
		
		/**
		 * Logic
		 */
		Logger.log("Updating TaskId: " + taskId + " to a new version.", 
				TaskList.class.getSimpleName(), "update()");
		
		if(list.get(index).update(t) != 0) {
			return Constants.TASKLIST_FAILED_UPDATE_TO_AL;
		}
		
		if(pastActions.add(index) != 0) {
			return Constants.TASKLIST_FAILED_UPDATE_TO_AH;
		}
		
		return saveToFile(getAll());
	}
	
	/**
	 * Retrieves and returns the index of the first occurrence of 
	 * the argument TaskId.
	 * 
	 * @param taskId The TaskId of the Task to be searched.
	 * @return Returns the index of the Task within the TaskList corresponding 
	 * to the given TaskId. Returns -1 if no matching Task was found.
	 */
	private int getIndexOf(int taskId) {
		for(int i = 0; i < list.size(); i++) {
			Task tempTask = null;
			
			if(list.get(i) != null && list.get(i).getTask() != null) {
				tempTask = list.get(i).getTask();
			}
			
			if(tempTask != null && tempTask.getId() == taskId) {
				return i;
			}
		}
				
		return -1;
	}
	
	/**
	 * Logically deletes the Task at the given index, 
	 * returning it to the calling method.
	 * 
	 * @param index The index of the Task within TaskList where 
	 * said Task is residing in.
	 * @return The Task that was deleted. Returns null on error.
	 */
	private Task deleteByIndex(int index) {
		/**
		 * Check argument
		 */
		if(checkIfWithinSizeOfTaskList(index) != 0) {
			return null;
		}	
		
		/**
		 * Logic
		 */
		Logger.log("Deleting i: " + index + " from TaskList", 
				TaskList.class.getSimpleName(), "deleteByIndex()");
		
		Task tempTask = list.get(index).delete();
		pastActions.add(index);
		
		/**
		 * If there's a failure to save, do nothing.
		 */
		saveToFile(getAll());
		
		return tempTask;
	}
	
	/**
	 * Logically deletes the first occurrence of the Task with 
	 * the given TaskId, returning it to the calling method.
	 * 
	 * @param taskId The TaskId of the Task to be deleted.
	 * @return The Task that was deleted. Returns null on error.
	 */
	public Task delete(int taskId) {
		/**
		 * Check argument
		 */
		if(taskId < 0) {
			return null;
		}
		
		/**
		 * Logic
		 */
		Logger.log("Deleting taskId: " + taskId + " from TaskList", 
				TaskList.class.getSimpleName(), "delete(int taskId)");
		
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && 
					list.get(i).getTask() != null &&
					list.get(i).getTask().getId() == taskId) {
				return deleteByIndex(i);
			}
		}

		return null;
	}
	
	/**
	 * Logically deletes the first occurrence of the Task with 
	 * the given title, returning it to the calling method.
	 * 
	 * @param keywordTitle The title of the Task to be deleted.
	 * @return The Task that was deleted. Returns null on error.
	 */
	public Task delete(String keywordTitle) {
		/**
		 * Check argument
		 */		
		if(checkIfNull(keywordTitle) != 0) {
			return null;
		}	
		
		/**
		 * Logic
		 */
		Logger.log("Deleting title: " + keywordTitle + " from TaskList", 
				TaskList.class.getSimpleName(), "delete(String keywordTitle)");
		
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && 
					list.get(i).getTask() != null &&
					list.get(i).getTask().getTitle() != null &&
					list.get(i).getTask().getTitle().equalsIgnoreCase(keywordTitle)) {
				return deleteByIndex(i);
			}
		}

		return null;
	}
	
	/**
	 * Undo the last action performed on Tasks in this TaskList.
	 * 
	 * @return Returns one of the following error codes:
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.NO_MODIFICATIONS_WERE_MADE, 
	 * Constants.UNABLE_TO_SAVE, 
	 * Constants.FILEHANDLER_FAILED_WRITE_FILE_DOES_NOT_EXIST, 
	 * Constants.INVALID_NULL_ARG or 
	 * Constants.IOEXCEPTION_TRIGGERED.
	 */
	public int undo() {
		Logger.log("Undoing last action.", 
				TaskList.class.getSimpleName(), "undo()");
		
		int lastIndex = pastActions.getIndexOfLastTaskActedOn();
		
		if(lastIndex >= 0) {
			list.get(lastIndex).undo();
			pastActions.undo();
			return saveToFile(getAll()); 
		} else {
			return Constants.NO_MODIFICATIONS_WERE_MADE;
		}
	}
	
	/**
	 * Redo the last action performed on Tasks in this TaskList 
	 * (i.e. undoing the last undo).
	 * 
	 * @return Returns one of the following error codes:
	 * Constants.SUCCESS_ERROR_CODE, 
	 * Constants.NO_MODIFICATIONS_WERE_MADE, 
	 * Constants.UNABLE_TO_SAVE, 
	 * Constants.FILEHANDLER_FAILED_WRITE_FILE_DOES_NOT_EXIST, 
	 * Constants.INVALID_NULL_ARG or 
	 * Constants.IOEXCEPTION_TRIGGERED.
	 */
	public int redo() {
		Logger.log("Redoing last action.", 
				TaskList.class.getSimpleName(), "redo()");
		
		pastActions.redo();
		int lastIndex = pastActions.getIndexOfLastTaskActedOn();
		
		if(lastIndex > 0) {
			list.get(lastIndex).redo();
			return saveToFile(getAll()); 
		} else {
			return Constants.NO_MODIFICATIONS_WERE_MADE;
		}
	}
	
	/**
	 * Retrieves and returns all instances of Tasks, in this TaskList, that
	 * contain the given keyword String in any string field. Case insensitive.
	 * 
	 * @param keyword Keyword String to search for. 
	 * @return An ArrayList of Task objects that matches the search 
	 * criteria. Returns null on error.
	 */
	public ArrayList<Task> searchFor(String keyword) {
		Logger.log("Searching for all instances of keyword: " + keyword, 
				TaskList.class.getSimpleName(), "searchFor(String keyword)");
		
		return searchWithin(getAll(), keyword);
	}
	
	/**
	 * Retrieves and returns all instances of Tasks in this TaskList that 
	 * contains ALL the keywords, in any string field, in the given String array. 
	 * Case insensitive.
	 * 
	 * @param keywordArr A String array containing all keywords to search for. 
	 * @return An ArrayList of Task objects that matches the search 
	 * criteria. Returns null on error.
	 */
	public ArrayList<Task> searchFor(String[] keywordArr) {
		/**
		 * Check argument
		 */		
		if(checkIfNull(keywordArr) != 0 || keywordArr.length == 0) {
			return null;
		}
		
		for(int i=0; i < keywordArr.length; i++) {			
			if(checkIfNull(keywordArr[i]) != 0) {
				return null;
			}
		}
		
		/**
		 * Logic
		 */
		Logger.log("Recursively searching for all instances of keywordArray", 
				TaskList.class.getSimpleName(), "searchFor(String[] keywordArr)");
		
		ArrayList<Task> tempTaskAL = getAll(); 
		
		for(int i = 0; i < keywordArr.length; i++) {
			tempTaskAL = searchWithin(tempTaskAL, keywordArr[i]);
		}
		
		return tempTaskAL;
	}
	
	/**
	 * Retrieves and returns all instances of Tasks, in the given ArrayList 
	 * of Task objects, that contain the given keyword String in any string 
	 * field. Case insensitive.
	 * 
	 * @param al ArrayList of Tasks to be searched.
	 * @param keyword Keyword String to search for. 
	 * @return An ArrayList of Task objects that matches the search 
	 * criteria. Returns null on error.
	 */
	private ArrayList<Task> searchWithin(ArrayList<Task> al, String keyword) {
		/**
		 * Check argument
		 */
		if(checkIfNull(keyword) != 0 || checkIfNull(al) != 0) {
			return null;
		}
		
		/**
		 * Logic
		 */
		Logger.log("Searching for all instances of keyword: " + keyword, 
				TaskList.class.getSimpleName(), "searchWithin(String keyword)");	
		
		ArrayList<Task> output = new ArrayList<Task>();
		Task tempTask;
		
		for(int i = 0; i < al.size(); i++) {
			if(al.get(i) != null) {
				tempTask = al.get(i);
				
				if(tempTask.getIsValid() &&
						((tempTask.getTitle() != null && 
							tempTask.getTitle().toLowerCase().contains(keyword.toLowerCase())) ||
						(tempTask.getDescription() != null && 
							tempTask.getDescription().toLowerCase().contains(keyword.toLowerCase()))
						)) {

					if(!output.add(tempTask)) {
						return null;
					}
				}
			}		
		} //end of for loop
		
		return output;
	}
		
	/**
	 * Retrieves and returns all instances of Tasks, in this TaskList, that
	 * contain the given keyword String in its title. Case insensitive.
	 * 
	 * @param keyword Keyword String to search for. 
	 * @return An ArrayList of Task objects that matches the search 
	 * criteria. Returns null on error.
	 */
	public ArrayList<Task> searchForTitle(String keyword) {
		/**
		 * Check argument
		 */		
		if(checkIfNull(keyword) != 0) {
			return null;
		}	
		
		/**
		 * Logic
		 */
		Logger.log("Searching for all instances of keyword: " + keyword + " in the title only.", 
				TaskList.class.getSimpleName(), "searchForTitle(String keyword)");
		
		ArrayList<Task> output = new ArrayList<Task>();
		Task tempTask;
		
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && list.get(i).getTask() != null) {
				tempTask = list.get(i).getTask();
			
				if(tempTask.getTitle() != null && 
						tempTask.getTitle().compareTo(keyword) == 0) {
					//Blank Line
					if(!output.add(tempTask)) {
						return null;
					}
				}
			}
		}
		return output;
	}
	
	/**
	 * Retrieves and returns all instances of Tasks, in this TaskList, that
	 * has the given TaskId.
	 * 
	 * @param taskId TaskId to search for. 
	 * @return An ArrayList of Task objects that matches the search 
	 * criteria. Returns null on error.
	 */
	public ArrayList<Task> searchForTaskId(int taskId) {
		/**
		 * Check arguments
		 * 
		 * No arguments to check
		 */
		
		/**
		 * Logic
		 */
		Logger.log("Searching for all instances of taskId: " + taskId, 
				TaskList.class.getSimpleName(), "searchForTaskID(int taskID)");
		
		ArrayList<Task> output = new ArrayList<Task>();
		Task tempTask;
		
		for(int i=0; i<list.size(); i++) {
			if(list.get(i) != null && list.get(i).getTask() != null) {
				tempTask = list.get(i).getTask();
				
				if(tempTask.getId() == taskId) {
					//Blank Line
					output.add(tempTask);
				}
			}
		}
		
		return output;
	}
}
